#include <JavaScriptCore/JavaScriptCore.h>
#include <iostream>
#include <sys/stat.h>

using namespace std;

struct FilesystemPrivate {
  string path;
  bool is_directory;
  bool is_file;
  bool is_symlink;
  size_t size;
  bool exists;
};

std::string JSStringToStdString(JSStringRef jsString) {
  size_t maxBufferSize = JSStringGetMaximumUTF8CStringSize(jsString);
  char *utf8Buffer = new char[maxBufferSize];
  size_t bytesWritten =
      JSStringGetUTF8CString(jsString, utf8Buffer, maxBufferSize);
  std::string utf_string = std::string(
      utf8Buffer,
      bytesWritten -
          1); // the last byte is a null \0 which std::string doesn't need.
  delete[] utf8Buffer;
  return utf_string;
}

JSValueRef ObjectCallAsFunctionCallback(JSContextRef ctx, JSObjectRef function,
                                        JSObjectRef thisObject,
                                        size_t argumentCount,
                                        const JSValueRef arguments[],
                                        JSValueRef *exception) {
  cout << "ObjectCallAsFunctionCallback <";
  for (size_t i = 0; i < argumentCount; i++) {
    JSStringRef pathString = JSValueToStringCopy(ctx, arguments[i], nullptr);
    cout << JSStringToStdString(pathString);
  }
  cout << endl;
  return JSValueMakeUndefined(ctx);
}

void setAttributes(FilesystemPrivate *fs, std::string path) {
  fs->path = path;

  struct stat statbuf;

  if (lstat(path.c_str(), &statbuf) != -1) {
    switch (statbuf.st_mode & S_IFMT) {
    case S_IFREG:
      fs->is_file = true;
      break;
    case S_IFLNK:
      fs->is_symlink = true;
      break;
    case S_IFDIR:
      fs->is_directory = true;
      break;
    }
    fs->size = statbuf.st_size;
    fs->exists = true;
  } else {
    fs->exists = false;
    fs->is_file = false;
    fs->is_directory = false;
    fs->is_symlink = false;
    fs->size = 0;
  }
}

/* callbacks */

void Filesystem_Finalize(JSObjectRef object) {
  FilesystemPrivate *fs =
      static_cast<FilesystemPrivate *>(JSObjectGetPrivate(object));
  delete fs;
}

JSObjectRef Filesystem_CallAsConstructor(JSContextRef ctx,
                                         JSObjectRef constructor,
                                         size_t argumentCount,
                                         const JSValueRef arguments[],
                                         JSValueRef *exception) {
  FilesystemPrivate *fs = new FilesystemPrivate();

  JSStringRef pathString = JSValueToStringCopy(ctx, arguments[0], nullptr);
  setAttributes(fs, JSStringToStdString(pathString));
  JSObjectSetPrivate(constructor, static_cast<void *>(fs));

  return constructor;
}

/* static values */

JSValueRef Filesystem_getPath(JSContextRef ctx, JSObjectRef object,
                              JSStringRef propertyName, JSValueRef *exception) {
  FilesystemPrivate *fs =
      static_cast<FilesystemPrivate *>(JSObjectGetPrivate(object));
  JSStringRef pathString = JSStringCreateWithUTF8CString(fs->path.c_str());

  return JSValueMakeString(ctx, pathString);
}

bool Filesystem_setPath(JSContextRef ctx, JSObjectRef object,
                        JSStringRef propertyName, JSValueRef value,
                        JSValueRef *exception) {
  FilesystemPrivate *fs =
      static_cast<FilesystemPrivate *>(JSObjectGetPrivate(object));
  JSStringRef pathString = JSValueToStringCopy(ctx, value, nullptr);

  setAttributes(fs, JSStringToStdString(pathString));

  return true;
}

JSValueRef Filesystem_getType(JSContextRef ctx, JSObjectRef object,
                              JSStringRef propertyName, JSValueRef *exception) {
  FilesystemPrivate *fs =
      static_cast<FilesystemPrivate *>(JSObjectGetPrivate(object));
  JSStringRef pathType;

  if (fs->is_file) {
    pathType = JSStringCreateWithUTF8CString("File");
  } else if (fs->is_directory) {
    pathType = JSStringCreateWithUTF8CString("Directory");
  } else if (fs->is_symlink) {
    pathType = JSStringCreateWithUTF8CString("Symlink");
  } else {
    pathType = JSStringCreateWithUTF8CString("Unknown");
  }

  return JSValueMakeString(ctx, pathType);
}

JSValueRef Filesystem_getExist(JSContextRef ctx, JSObjectRef object,
                               JSStringRef propertyName,
                               JSValueRef *exception) {
  FilesystemPrivate *fs =
      static_cast<FilesystemPrivate *>(JSObjectGetPrivate(object));

  return JSValueMakeBoolean(ctx, fs->exists);
}

JSValueRef Filesystem_getSize(JSContextRef ctx, JSObjectRef object,
                              JSStringRef propertyName, JSValueRef *exception) {
  FilesystemPrivate *fs =
      static_cast<FilesystemPrivate *>(JSObjectGetPrivate(object));

  return JSValueMakeNumber(ctx, static_cast<double>(fs->size));
}

JSValueRef Filesystem_remove(JSContextRef ctx, JSObjectRef function,
                             JSObjectRef object, size_t argumentCount,
                             const JSValueRef arguments[],
                             JSValueRef *exception) {
  FilesystemPrivate *fs =
      static_cast<FilesystemPrivate *>(JSObjectGetPrivate(object));
  remove(fs->path.c_str());

  return JSValueMakeUndefined(ctx);
}

JSClassRef FilesystemClass() {
  static JSClassRef filesystem_class;
  if (!filesystem_class) {
    JSClassDefinition classDefinition = kJSClassDefinitionEmpty;

    static JSStaticFunction staticFunctions[] = {
        {"remove", Filesystem_remove,
         kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete},
        {0, 0, 0}};

    static JSStaticValue staticValues[] = {
        {"path", Filesystem_getPath, Filesystem_setPath,
         kJSPropertyAttributeDontDelete},
        {"type", Filesystem_getType, 0,
         kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete},
        {"exists", Filesystem_getExist, 0,
         kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete},
        {"size", Filesystem_getSize, 0,
         kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete},
        {0, 0, 0, 0}};

    classDefinition.className = "Filesystem";
    classDefinition.attributes = kJSClassAttributeNone;
    classDefinition.staticFunctions = staticFunctions;
    classDefinition.staticValues = staticValues;
    classDefinition.finalize = Filesystem_Finalize;
    classDefinition.callAsConstructor = Filesystem_CallAsConstructor;

    filesystem_class = JSClassCreate(&classDefinition);
  }
  return filesystem_class;
}

int main(int argc, const char *argv[]) {

  JSContextGroupRef contextGroup = JSContextGroupCreate();
  JSGlobalContextRef globalContext =
      JSGlobalContextCreateInGroup(contextGroup, FilesystemClass());
  JSObjectRef globalObject = JSContextGetGlobalObject(globalContext);

  JSValueRef exception = nullptr;

  JSObjectRef functionObject = JSObjectMakeFunctionWithCallback(
      globalContext, JSStringCreateWithUTF8CString("log"),
      ObjectCallAsFunctionCallback);
  JSObjectSetProperty(globalContext, globalObject,
                      JSStringCreateWithUTF8CString("log"), functionObject,
                      kJSPropertyAttributeNone, &exception);

  if (exception != nullptr) {
    JSStringRef exStr = JSValueToStringCopy(globalContext, exception, nullptr);
    size_t len = JSStringGetLength(exStr);
    char exChars[len];
    JSStringGetUTF8CString(exStr, exChars, len);
    if (exStr)
      JSStringRelease(exStr);
    printf("set log function in globalObjectl %s\n", exChars);
  }

  JSObjectRef filesystemObject = JSObjectMake(globalContext,
  FilesystemClass(), nullptr); JSObjectSetProperty(globalContext,
  globalObject, JSStringCreateWithUTF8CString("Filesystem"),
  filesystemObject, kJSPropertyAttributeNone, nullptr);

  JSEvaluateScript(
      globalContext,
      JSStringCreateWithUTF8CString(
          "var fs = new Filesystem('/Users/{user}/Desktop/file');log('invoke "
          "in js: ' + fs.exists);"),
      nullptr, nullptr, 1, &exception);

  if (exception != nullptr) {
    JSStringRef exStr = JSValueToStringCopy(globalContext, exception, nullptr);
    size_t len = JSStringGetLength(exStr);
    char exChars[len];
    JSStringGetUTF8CString(exStr, exChars, len);
    if (exStr)
      JSStringRelease(exStr);
    printf("execute globalObject predefined static function: %s\n", exChars);
  }
  return 0;
}